﻿/*
 * Copyright the original author or authors.
 * 
 * Licensed under the MOZILLA PUBLIC LICENSE, Version 1.1 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.mozilla.org/MPL/MPL-1.1.html
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import com.bourre.commands.Delegate;
import com.bourre.events.EventBroadcaster;
import com.bourre.events.BasicEvent;
import com.bourre.events.EventType;
import com.bourre.log.PixlibStringifier;
import com.bourre.transitions.IFrameListener;
import com.bourre.transitions.IFrameBeacon;
import com.bourre.transitions.FPSBeacon;

import com.kairos.engine.RTEvent;

/**
 * The {@code RTBeacon} work as an {@code IFrameBeacon} object but send
 * a {@link com.kairos.engine.RTEvent} in the {@code onEnterFrame} method.
 *  * 
 * <p>The {@code RTBeacon} can be plugged on any other beacon,but by default
 * the beacon operate on the swf framerate with the {code FPSBeacon}.</p>
 * 
 * <p>The paradigm behind the {@code RTBeacon} is that all objects
 * have to evolve in time not by frame but according to a real time unit,
 * in that way objects animations can't be affected by framerate variations.
 * <br/>
 * The basic approach is to define all equations on a second-based unit.
 * Then, even if the framerate change during time or according browser 
 * restrictions, all objects will move at the same speed.</p>
 * 
 * <p>The {@code RTBeacon} object provide some method to control <i>time scale</i> and
 * <i>smoothness</i> directly from the source, when using those methods all objects are
 * affected by the changes.
 * <br/><ul>
 * <li>Time scale is controlled by a global speed modificator witch allow to multiply
 * or divide the speed of animations. The dispatched timestep is allways modified by
 * the global speed parameter.</li>
 * <li>Smoothness control is done by two settings, the <i>maximum step size</i> and the
 * <i>smooth</i> parameter. The first control the maximum value the {@code RTBeacon} 
 * dispatch with the {@code onEnterFrame} event, the second control the regularity of 
 * the step value by returning an average value of a limited number of frames. The number
 * of frames used to calculate the average value is equals to the <i>getSmoothness</i> 
 * return.</li>
 * </ul></p>
 * 
 * <p>The {@code RTBeacon} is a singleton object. Use the {@link RTBeacon#getInstance}
 * method to get a reference to it.</p>
 * 
 * @author Cédric Néhémie
 * @example import com.bourre.transitions.IFrameListener;
 * 
 * import com.kairos.engine.RTEvent;
 * import com.kairos.engine.RTBeacon;
 * import com.kairos.engine.RTObject;
 * 
 * // A simple class that move an object on X axis
 * // based on the timestep dispatched by the RTBeacon.
 * // Here moves are write in px/s
 * class Mover extends RTObject implements IFrameListener
 * {
 * 	private var _oTarget : MovieClip; // Object to move.
 * 	private var _nSpeed : Number; // Speed in px/s.
 * 
 * 	public function Mover ( o : MovieClip, speed : Number )
 * 	{
 * 		_nSpeed = speed > 0 ? speed : 1;
 *		_oTarget = o;
 * 			
 * 		// start the animation
 * 		start();
 * 	} 
 * 	public function onEnterFrame () : Void
 * 	{
 * 		// current speed computed with the RTEvent.getStepSecond() method
 * 		var curSpeed : Number = _nSpeed * ( RTEvent ( arguments[ 0 ] ).getStepSecond() ); 
 * 
 * 		_oTarget._x += curSpeed;
 * 	}
 * }
 */ 
class com.kairos.engine.RTBeacon implements IFrameBeacon, IFrameListener
{
	/**
	 * Broadcasted at each frame displayed by the player.
	 */
	public static var onEnterFrameEVENT : EventType = new EventType( 'onEnterFrame' );
	
	private static var _oI : RTBeacon;
	
	/**
	 * Returns a reference to the {@code RTBeacon} instance.
	 *	
	 * @return	The {@code RTBeacon} instance.
	 * @example	var rtInstance : RTBeacon = RTBeacon.getInstance();
	 */	
	public static function getInstance () : RTBeacon
	{
		if ( _oI == null )
		{
			_oI = new RTBeacon ();
		}
		return _oI;
	}
	/**
	 * Destroy the {@code RTBeacon} instance.
	 * 
	 * <p>Warning {@code onEnterFrame} event will never be 
	 * broadcasted.</p>
	 */
	public static function release() : Void
	{
		_oI.stop();
		_oI._mc.removeMovieClip();
		delete _oI;
	}
	
	/*
	 * Eventbroadcasting methods.
	 */
	private var addListener:Function;
	private var removeListener:Function;
	private var broadcastMessage:Function;
	private var _listeners:Array;
	
	
	// Frame count
	private var _nC : Number;
	// Frame duration
	private var _nS : Number;
	// Frame duration in seconds
	private var _nSc : Number;
	// Last frame time
	private var _nMs : Number;
	// Max step size
	private var _nMSS : Number;
	// Global Speed
	private var _nGS : Number;
	// Is playing ? 
	private var _bIP : Boolean;
	// Event object.
	private var _e : RTEvent;
	// Intenal MovieClip
	private var _mc : MovieClip;
	//Original Beacon
	private var _oBeacon : IFrameBeacon; 
	// Smooth amount
	private var _nSmooth : Number;
	// Smooth array
	private var _aSmoothValues : Array;
	// Smooth value sum
	private var _nSmoothSum : Number;
	
	/**
	 * @private
	 */
	private function RTBeacon ()
	{
		AsBroadcaster.initialize(this);
		
		_mc = _level0.createEmptyMovieClip ( "__mcRTBeacon", 9995 );
		_e = new RTEvent ( onEnterFrameEVENT, this );
		_oBeacon = FPSBeacon.getInstance();
		_nGS = 1;
		_nC = 0;
		_nS = 0;
		_nSc = 0;
		_nMs = getTimer();
		_nMSS = Number.POSITIVE_INFINITY;
		_aSmoothValues = [];
		_nSmooth = 0;
		_nSmoothSum = 0;
	}
	
	/**
	 * Proceed all times operation and broadcast a {@link com.kairos.engine.RTEvent}.
	 */	
	public function onEnterFrame () : Void
	{
		var cT:Number = getTimer();
		
		_nS = ( cT - _nMs ) * _nGS;
		
		if( _nS > _nMSS ) _nS = _nMSS;
		if( _nSmooth ) _nS = _smooth ( _nS );
		
		_nSc = _nS / 1000;
		
		_e.setStep ( _nS );
		_e.setStepInSecond ( _nSc );
		
		broadcastMessage ( _e.getType(), _e );
		
		_nMs = cT;
		_nC++;
	}
	
	/**
	 * Define a maximum value for the {@code getStep} return value.
	 * 
	 * <p>Use that parameter to limit the lag incidence. When the frame
	 * duration becomes too high, animations will make a big gap between each
	 * refresh. It can really be a big issue in games, where many collisions
	 * detection will failed if the object pass through an object during
	 * the move.</p>
	 * 
	 * <p>If the parameter isn't specified, the value is set to
	 * {@code Number.POSITIVE_INFINITY}.</p>
	 *	
	 * @param	n Maximum time step in milliseconds.
	 * @example	RTBeacon.getInstance().setMaxStepSize( 100 );
	 */	
	public function setMaxStepSize ( n : Number ) : Void
	{
		_nMSS = n > 0 ? n : Number.POSITIVE_INFINITY;
	}
	
	/**
	 * Return the current maximum timestep size in milliseconds.
	 * 
	 * <p>By default return {@code Number.POSITIVE_INFINITY}.</p>
	 *	
	 * @return	{@code Number}
	 * @example	var maxStepSize : Number = RTBeacon.getInstance().getMaxStepSize();
	 */	
	public function getMaxStepSize () : Number
	{
		return _nMSS;
	}
	
	/**
	 * Define a speed rate for the {@code RTBeacon}, it works as a
	 * multiplier for timesteps.
	 * 
	 * <p>By default the speed rate is set to 1, you can use this parameter
	 * to create time stretching effects.</p>
	 * 
	 * <p>You cannot specify values lower or equals than 0</p>
	 * 
	 * @param   n 	A new speed rate fot the {@code RTBeacon}.
	 * @example	// Divide the speed by 2
	 * RTBeacon.getInstance().setGlobalSpeed ( .5 );
	 * @example	// Multiply the speed by 2
	 * RTBeacon.getInstance().setGlobalSpeed ( 2 );
	 */
	public function setGlobalSpeed ( n : Number ) : Void
	{
		if( n <= 0 ) return;

		_nGS = n;
	}
	
	/**
	 * Return the current speed rate of the {@code RTBeacon}.
	 * 
	 * @return	{@code Number} modificator for speed.
	 * @example	var globalSpeed : Number = RTBeacon.getInstance().getGlobalSpeed ();
	 */
	public function getGlobalSpeed (): Number
	{
		return _nGS;
	}
	
	/**
	 * Define the amount of smouthness of the {@code RTBeacon}
	 * step value.
	 * 
	 * <p>Set <i>smooth</i> on 0 to deactivate all smooth calculations.</p>
	 * 
	 * <p>Smoothness is basically the average of a limited number
	 * of timestep duration. The more values there is in the average
	 * calculation the less the resulting timestep may vary from a frame
	 * to the other.</p>
	 * 
	 * <p>The smooth parameter could create really awful side-effects.
	 * If you define smooth without setting a maximum step size, a big
	 * lag may does the animation speed up after the lag, and in that
	 * case, the more high the smooth is the more the speed up effect will
	 * run for a long time.</p> 
	 * 
	 * <p>That effects is reduced when arriving to really
	 * high smooth value (smooth > 1000) because a too long frame don't represent
	 * a great part of the average value, but in that case you will loose a part
	 * of the beacon's capability to adjust itself to the computer resulting 
	 * framerate, if a process will reduce the framerate by 2 the beacon don't
	 * reflect the change immediatly, resulting that animation may slow down until
	 * all values used to calculate the average time are in the current framerate
	 * time range.</p>
	 * 
	 * <p><strong>Note :</strong> When using smooth try to define a maximum step
	 * size about two times the espected frame duration.</p>
	 * 
	 * @param	smooth	The amount of smoothness in step values.
	 * @example	RTBeacon.getInstance().setSmoothness ( 10 );
	 */
	public function setSmoothness ( smooth : Number ) : Void
	{
		if( !smooth || smooth < 0 ) smooth = 0;
		
		_nSmooth = smooth;
		
		var l : Number = _aSmoothValues.length;
		if( l > _nSmooth )
		{
			l = l - _nSmooth;
			while ( --l -(-1 ) )
				_nSmoothSum -= Number ( _aSmoothValues.shift() );
		}
	}
	
	/**
	 * Returns the current smoothness amount.
	 * 
	 * @return	{@code Number} amount of smooth.
	 * @example	var smooth : Number = RTBeacon.getInstance().getSmoothness ();
	 */
	public function getSmoothness () : Number
	{
		return _nSmooth;
	}
	
	/**
	 * Used to perform the smooth
	 */
	private function _smooth ( n : Number) : Number
	{
		_aSmoothValues.push( n );
		var l : Number = _aSmoothValues.length;
		if( l > _nSmooth )
		{
			_nSmoothSum -= Number( _aSmoothValues.shift() );
			l--;
		}
		_nSmoothSum += n;
		
		return _nSmoothSum / l;
	} 
	
	/**
	 * Return the last frame duration in milliseconds.
	 * 
	 * @example	var seconds : Number = RTBeacon.getInstance().getStep ();
	 * @return	{@code Number} of milliseconds since
	 * 			the last {@code onEnterFrame} call.
	 */	
	public function getStep () : Number
	{
		return _nS;
	}
	
	/**
	 * Return the last frame duration in seconds.
	 *
	 * @example	var seconds : Number = RTBeacon.getInstance().getStepInSecond ();
	 * @return	{@code Number} of seconds since
	 * 			the last {@code onEnterFrame} call.
	 */	
	public function getStepInSecond () : Number
	{
		return _nSc;
	}
	
	/**
	 * Return the number of frames the {@code RTBeacon} run.
	 *	
	 * @return {@code Number} of frames since the 
	 * 		   {@code RTBeacon} start.
	 * @example	var count : Number = RTBeacon.getInstance().getCount ();
	 */	
	public function getCount () : Number
	{
		return _nC;
	}
	
	/**
	 * Starts the process.
	 * 
	 * <p>Example
	 * <code>
	 *  RTBeacon.getInstance().start();
	 * </code>
	 */
	public function start() : Void
	{
		_nMs = getTimer();
		_bIP = true;
		_oBeacon.addFrameListener( this );
	}
	
	/**
	 * Stops the process.
	 * 
	 * <p>Example
	 * <code>
	 *   RTBeacon.getInstance().stop();
	 * </code>
	 */
	public function stop() : Void
	{
		_oBeacon.removeFrameListener( this );
		_bIP = false;
	}
	
	/**
	 * Indicates if RTBeacon's running.
	 * 
	 * <p>Example
	 * <code>
	 *   var b:Boolean = RTBeacon.getInstance().isPlaying();</em>
	 * </code>
	 * 
	 * @return {@code true} if {@code RTBeacon} is running, either {@code false}
	 */
	public function isPlaying() : Boolean
	{
		return _bIP;
	}
	
	/**
	 * Define on witch beacon the {@code RTBeacon} run.
	 * 
	 * <p>By default the {@code RTBeacon} is plugged on
	 * the {@code FPSBeacon} of Pixlib, but you can swap to any
	 * other beacon at any time.</p>
	 * 
	 * <p>The time computation in the beacon isn't affected by the
	 * way the reference beacon work. So don't use an {@code FLVBeacon}
	 * to synchronize a tweens with a video within Kairos, just use what
	 * Pixlib allready provide.</p>
	 * 
	 * @param	b	{@code IFrameBeacon}
	 * @example	RTBeacon.getInstance().setFrameBeacon ( MSBeacon.getInstance() );
	 */
	public function setFrameBeacon ( b : IFrameBeacon ) : Void
	{
		if( b )
		{
			if( _bIP )
			{
				stop();
				if( b ) _oBeacon = b;
				start();
			}
			else _oBeacon = b;
		}
		
	}
	
	/**
	 * Adds listener for receiving all {@code RTBeacon} events.
	 * 
	 * <p>Example
	 * <code>
	 *   var b : RTBeacon = RTBeacon.getInstance();
	 *   b.addFrameListener( myListener );
	 * </code>
	 * 
	 * @param oL Listener object which implements {@link IFrameListener} interface.
	 */
	public function addFrameListener( oL : IFrameListener ) : Void
	{
		if ( _listeners.length < 1 ) start();
		addListener( oL );
	}
	
	/**
	 * Removes passed-in listener for receiving all {@code RTBeacon} events.
	 * 
	 *  <p>Example
	 * <code>
	 *   var b : RTBeacon = RTBeacon.getInstance();
	 *   b.removeFrameListener( myListener );
	 * </code>
	 * 
	 * @param oL Listener object which implements {@link IFrameListener} interface.
	 */
	public function removeFrameListener( oL : IFrameListener ) : Void
	{
		removeListener( oL );
		if ( _listeners.length < 1 ) this.stop();
	}
	
	/**
	 * Return the String representation of the object.
	 */	
	public function toString() : String
	{
		return PixlibStringifier.stringify( this );
	}
}


